package main

import (
	"bytes"
	"crypto/md5"
	"errors"
	"fmt"
	"github.com/op/go-logging"
	"github.com/satori/go.uuid"
	"html/template"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type TplParams struct {
	AppName    string
	Plist      string
	IPA        string
	Cover      string
	AppVersion string
}

type Generator struct {
	Conf          *PlistConfig
	uuid          string
	tempDir       string
	loggerBackEnd *logging.MemoryBackend
}

type PlistSetLocal struct {
	list []*AppAsset
	conf *PlistConfig
	doc  *PlistXMLDocument
}

type PListSetRemote struct {
	PlistSetLocal
	tempDir string
}

func NewGenerator(conf *PlistConfig) *Generator {
	tid := fmt.Sprintf("%s", uuid.NewV4())

	var format = logging.MustStringFormatter(
		`generator %{time:2006-01-02T15:04:05-0700} >> %{id:03x} %{level:.4s} %{message}`,
	)

	backend := logging.NewMemoryBackend(20)
	backendFormatter := logging.NewBackendFormatter(backend, format)
	backend1Leveled := logging.AddModuleLevel(backendFormatter)
	backend1Leveled.SetLevel(logging.INFO, tid)

	log.AddBackEnd(&BackEndItem{
		ID: tid,
		BackEnd: backend1Leveled,
	})

	return &Generator{
		Conf:          conf,
		uuid:          tid,
		tempDir:       filepath.Join(conf.TempPath, tid),
		loggerBackEnd: backend,
	}
}

func (this *Generator) LoadPlistURL(url string) (*PListSetRemote, error) {
	local, err := DownloadRemoteFile(url, this.tempDir)
	if err != nil {
		return nil, err
	}
	doc, err := NewPlistXMLDocument(local)
	if err != nil {
		return nil, err
	}

	assets := make([]*AppAsset, 0)
	if len(doc.Items) > 0 {
		first := doc.Items[0]

		if len(first.Assets) > 0 {
			for _, asset := range first.Assets {
				assets = append(assets, &AppAsset{
					URL:  asset.URL,
					Kind: asset.Kind,
				})
			}
		}
	}

	clonePlistPath := filepath.Join(this.tempDir, doc.GetAppBundleName(), filepath.Base(local))
	cloneDir := filepath.Dir(clonePlistPath)
	if _, err2 := os.Stat(cloneDir); os.IsNotExist(err2) {
		err = os.MkdirAll(cloneDir, 0777)
		if err != nil {
			log.Error(err)
			return nil, err
		}
	}
	err = os.Rename(local, clonePlistPath)
	if err != nil {
		log.Error(err)
		return nil, err
	}

	assets = append(assets, &AppAsset{
		Path: clonePlistPath,
		Ext:  filepath.Ext(clonePlistPath),
		Kind: ASSET_KIND_PLIST,
	})

	conf := this.Conf.Clone()
	conf.BasePath = this.tempDir

	return &PListSetRemote{
		PlistSetLocal: PlistSetLocal{
			conf: conf,
			doc:  doc,
			list: assets,
		},
		tempDir: filepath.Join(this.tempDir, doc.GetAppBundleName()),
	}, nil
}

func getRemoteFileContent(remoteURL string) (content string, err error) {
	log.Info("begin get remote file content, url:", remoteURL)

	resp, err := http.Get(remoteURL)
	if err != nil {
		log.Error(err)
		return
	}
	defer resp.Body.Close()

	bin, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Error(err)
		return
	}

	return string(bin), nil
}

//下载远程文件
func DownloadRemoteFile(remoteURL string, tempDir string) (localPath string, err error) {
	log.Info("begin download file, url:", remoteURL)
	var ext string
	urlInfo, err := url.Parse(remoteURL)
	if err != nil {
		log.Error(err)
		return
	}
	ext = filepath.Ext(urlInfo.Path)
	if !strings.Contains(ext, ".") {
		ext = ""
	}
	filename := fmt.Sprintf("%x-%d%s", md5.Sum([]byte(remoteURL)), rand.Intn(100),ext)
	localPath = filepath.Join(tempDir, filename)

	if _, err2 := os.Stat(tempDir); os.IsNotExist(err2) {
		err = os.MkdirAll(tempDir, 0777)
		if err != nil {
			log.Error(err)
			return
		}
	}

	file, err := os.Create(localPath)
	if err != nil {
		log.Error(err)
		return
	}
	defer file.Close()

	resp, err := http.Get(remoteURL)
	if err != nil {
		log.Error(err)
		return
	}
	defer resp.Body.Close()

	_, err = io.Copy(file, resp.Body)
	if err != nil {
		log.Error(err)
	}
	return
}

func (this *Generator) GetUUID() string {
	return this.uuid
}

func (this *Generator) Scan(dir string) (*PlistSetLocal, error) {
	localPath := filepath.Join(dir, "/*")

	_, err := filepath.Rel(this.Conf.BasePath, dir)
	if err != nil {
		log.Error(err)
		localPath = filepath.Join(this.Conf.BasePath, dir, "/*")
	}

	log.Info("begin scan asset in", localPath)

	assets := LoadAppAssets(localPath)
	if len(assets) > 0 {
		set := &PlistSetLocal{
			list: assets,
			conf: this.Conf,
		}

		err := set.Load()
		if err != nil {
			return nil, err
		}

		log.Info("loaded all asset", set.list)

		return set, nil
	}

	return nil, errors.New("no asset found")
}

func (this *Generator) Clear() {
	if _, err := os.Stat(this.tempDir); err == nil {
		err := os.RemoveAll(this.tempDir)
		if err != nil {
			log.Error(err)
		}
	}
	log.RemoveBackEnd(&BackEndItem{
		ID: this.uuid,
		BackEnd: this.loggerBackEnd,
	})
}

func (this *PlistSetLocal) Load() error {
	log.Info("begin parse plist")

	plist := GetAsset(ASSET_KIND_PLIST, this.list)
	if plist != nil {
		doc, err := NewPlistXMLDocument(plist.Path)
		if err != nil {
			return err
		}
		this.doc = doc
		return nil
	}

	return errors.New("plist file not found")
}

//获取最有合适的ipa文件
func (this *PlistSetLocal) selectIPA() *AppAsset {
	list := make([]*AppAsset, 0)
	filenameInPlist := filepath.Base(this.doc.GetAsset(ASSET_KIND_SOFTWARE_PACKAGE).URL)
	for _, item := range this.list {
		if item.Kind == ASSET_KIND_SOFTWARE_PACKAGE {
			list = append(list, item)

			if filepath.Base(item.Path) == filenameInPlist {
				return item
			}
		}
	}

	if len(list) > 0 {
		return list[0]
	}

	return nil
}

func (this *PlistSetLocal) UploadAsset(kind string, storage IStorage) error {
	var asset *AppAsset
	if kind == ASSET_KIND_SOFTWARE_PACKAGE {
		asset = this.selectIPA()
	} else {
		asset = GetAsset(kind, this.list)
	}

	if asset != nil {
		key, err := filepath.Rel(this.conf.BasePath, asset.Path)
		if err != nil {
			return err
		}

		savePath := filepath.Join(filepath.Dir(key), this.doc.GetVersion(), filepath.Base(key))

		path, url, err := storage.Upload(asset.Path, savePath)
		if err != nil {
			return err
		}

		if this.conf.CloudFront.Enable {
			url = fmt.Sprintf("%s%s", this.conf.CloudFront.Host, path)
		}

		item := this.doc.GetAsset(kind)
		if item != nil {
			item.URL = url
			return nil
		}
	}

	return errors.New("asset not found")
}

func (this *PlistSetLocal) loadTemplateContent() string {
	log.Info("begin load html template")
	html, err := ioutil.ReadFile(this.conf.Template)
	if err != nil {
		log.Error(err)
		return ""
	}

	return string(html)
}

func (this *PlistSetLocal) GenerateIndexHTML(plistURL string) (string, error) {
	log.Info("begin create index.html")

	tmpl := template.New("index-html")
	_, err := tmpl.Parse(this.loadTemplateContent())
	if err != nil {
		return "", err
	}

	buf := new(bytes.Buffer)
	params := TplParams{
		AppName:    this.doc.GetAppName(),
		Plist:      plistURL,
		IPA:        this.doc.GetAsset(ASSET_KIND_SOFTWARE_PACKAGE).URL,
		Cover:      this.doc.GetAsset(ASSET_KIND_FULL_SIZE_IMAGE).URL,
		AppVersion: this.doc.GetVersion(),
	}

	err = tmpl.Execute(buf, params)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}

func (this *PListSetRemote) DownloadToLocal() (err error) {
	queue := make(chan bool, 0)
	defer close(queue)
	counter := 0

	for i, asset := range this.list {
		if len(asset.Path) <= 0 {
			counter++

			go func(url string, index int) {
				defer func() {
					queue <- true
				}()
				ertriedCount := 0
				retry:
				local, err := DownloadRemoteFile(url, this.tempDir)
				if err != nil {
					log.Error(err)
					ertriedCount++
					if ertriedCount < 3 {
						log.Info("retry 2 download file.")
						goto retry
					}
					return
				}
				this.list[index].Path = local
				this.list[index].Ext = filepath.Ext(local)
			}(asset.URL, i)
		}
	}

	for counter > 0 {
		<-queue
		counter--
	}

	return
}

func (this *PlistSetLocal) UploadAll() (string, error) {
	queue := make(chan bool, 0)
	defer close(queue)
	jobCount := 0

	storage, err := NewS3Storage(this.conf.Storage.S3)
	if err != nil {
		return "", err
	}

	jobCount++
	go func() {
		defer func() {
			queue <- true
		}()
		log.Info("begin upload", ASSET_KIND_SOFTWARE_PACKAGE)
		err := this.UploadAsset(ASSET_KIND_SOFTWARE_PACKAGE, storage)
		if err != nil {
			log.Error(err)
			return
		}
	}()

	jobCount++
	go func() {
		defer func() {
			queue <- true
		}()

		log.Info("begin upload", ASSET_KIND_DISPLAY_IMAGE)
		err := this.UploadAsset(ASSET_KIND_DISPLAY_IMAGE, storage)
		if err != nil {
			log.Error(err)
			return
		}
	}()

	jobCount++
	go func() {
		defer func() {
			queue <- true
		}()

		log.Info("begin upload", ASSET_KIND_FULL_SIZE_IMAGE)
		err := this.UploadAsset(ASSET_KIND_FULL_SIZE_IMAGE, storage)
		if err != nil {
			log.Error(err)
			return
		}
	}()

	for jobCount > 0 {
		<-queue
		jobCount--
	}

	asset := GetAsset(ASSET_KIND_PLIST, this.list)
	if asset != nil {
		key, err := filepath.Rel(this.conf.BasePath, asset.Path)
		if err != nil {
			return "", err
		}
		key = filepath.Join(filepath.Dir(key), this.doc.GetVersion(), filepath.Base(key))
		this.doc.GetMate().Creator = "IPA2S3"
		this.doc.GetMate().CreatedAt = time.Now().Format(time.RFC3339)
		xml, err := this.doc.ToString()
		if err != nil {
			return "", err
		}

		log.Info("begin upload", ASSET_KIND_PLIST)
		path, url, err := storage.PutContent(xml, key, &UploadOptions{
			ContentType: "text/xml",
		})
		if err != nil {
			log.Error(err)
			return "", err
		}

		if this.conf.CloudFront.Enable {
			url = fmt.Sprintf("%s%s", this.conf.CloudFront.Host, path)
		}
		this.doc.GetMate().PlistURL = url

		indexKey, err := filepath.Rel(this.conf.BasePath, filepath.Join(filepath.Dir(asset.Path),
			this.doc.GetVersion(), "index.html"))
		if err != nil {
			return "", err
		}

		html, err := this.GenerateIndexHTML(url)
		if err != nil {
			log.Error(err)
			return "", err
		}
		log.Info("begin upload", "index.html")
		path, indexURL, err := storage.PutContent(html, indexKey, &UploadOptions{
			ContentType: "text/html",
		})
		if err != nil {
			log.Error(err)
			return "", err
		}

		if this.conf.CloudFront.Enable {
			indexURL = fmt.Sprintf("%s%s", this.conf.CloudFront.Host, path)
		}
		this.doc.GetMate().DownloadURL = indexURL

		return indexURL, nil
	}

	return "", errors.New("upload error")
}

func (this *PlistSetLocal) SaveToLocal() (string, error) {
	asset := GetAsset(ASSET_KIND_PLIST, this.list)
	if asset != nil {
		key, err := filepath.Rel(this.conf.BasePath, asset.Path)
		if err != nil {
			log.Error(err)
			return "", err
		}
		savePath := filepath.Join(this.conf.BasePath, key)
		savePath = filepath.Join(filepath.Dir(savePath), this.doc.GetVersion(), filepath.Base(savePath))

		saveDir := filepath.Dir(savePath)
		if _, err := os.Stat(saveDir); os.IsNotExist(err) {
			os.MkdirAll(saveDir, 0777)
		}
		xml, err := this.doc.ToString()
		if err != nil {
			log.Error(err)
			return "", err
		}
		return savePath, ioutil.WriteFile(savePath, []byte(xml), 0777)
	}

	return "", errors.New("no plist found.")
}

func (this *PListSetRemote) Assign(assets []*PlistAsset)  {
	for _, asset := range assets {
		if ASSET_KIND_PLIST == asset.Kind {
			continue
		}
		
		taregt1 := GetAsset(asset.Kind, this.list)
		if taregt1 != nil {
			taregt1.URL = asset.URL
		}
		target2 := this.doc.GetAsset(asset.Kind)
		if target2 != nil {
			target2.URL = asset.URL
		}
	}
}

func (this *PListSetRemote) UploadPlistAndHTML() (string, string, error) {
	storage, err := NewOSSStorage(this.conf.Storage.OSS)
	if err != nil {
		log.Error(err)
		return "", "", err
	}
	
	asset := GetAsset(ASSET_KIND_PLIST, this.list)
	if asset != nil {
		key, err := filepath.Rel(this.conf.BasePath, asset.Path)
		if err != nil {
			log.Error(err)
			return "", "", err
		}
		key = filepath.Join(filepath.Dir(key), this.doc.GetVersion(), filepath.Base(key))
		this.doc.GetMate().Creator = "IPA2S3"
		this.doc.GetMate().CreatedAt = time.Now().Format(time.RFC3339)
		xml, err := this.doc.ToString()
		if err != nil {
			log.Error(err)
			return "", "", err
		}
		
		log.Info("begin upload", ASSET_KIND_PLIST)
		_, url, err := storage.PutContent(xml, key, &UploadOptions{
			ContentType: "text/xml",
		})
		if err != nil {
			log.Error(err)
			return "", "", err
		}
		
		this.doc.GetMate().PlistURL = url
		log.Infof(`generate plist URL:%s`, url)
		
		indexKey, err := filepath.Rel(this.conf.BasePath, filepath.Join(filepath.Dir(asset.Path),
			this.doc.GetVersion(), "index.html"))
		if err != nil {
			log.Error(err)
			return "", "", err
		}
		
		html, err := this.GenerateIndexHTML(url)
		if err != nil {
			log.Error(err)
			return "", "", err
		}
		log.Info("begin upload", "index.html")
		_, indexURL, err := storage.PutContent(html, indexKey, &UploadOptions{
			ContentType: "text/html",
		})
		if err != nil {
			log.Error(err)
			return "", "", err
		}

		this.doc.GetMate().DownloadURL = indexURL
		log.Infof(`generate HTML Page URL:%s`, indexURL)
		
		return url, indexURL, nil
	}
	
	return "", "", errors.New("upload error")
}

func (this *Generator) ReadLog() *bytes.Buffer {
	node := this.loggerBackEnd.Head()
	buf := new(bytes.Buffer)
	for node != nil {
		buf.WriteString(node.Record.Formatted(0))
		buf.WriteString("\n")
		node = node.Next()
	}

	return buf
}

func (this *Generator) UploadWithAssetList(assets []*PlistAsset) (PlistURL string, PageURL string, err error) {
	
	var plistSet *PListSetRemote
	
	for _, asset := range assets {
		if ASSET_KIND_PLIST == asset.Kind {
			plistSet, err = this.LoadPlistURL(asset.URL)
			if err != nil {
				log.Error(err)
				return "", "", err
			}
		}
	}
	
	if plistSet == nil {
		return "", "", errors.New("can not found plist url")
	}
	
	plistSet.Assign(assets)
	
	return plistSet.UploadPlistAndHTML()
}
